Conversation
Adds the mcp scope group with a single access permission so it shows up in GET /api/v1/routes (and therefore in the frontend token form). Adds APIToken.HasMCPAccess() mirroring the caldav/feeds helpers. The MCP endpoint will use POST, GET, and DELETE on the same path for the streamable-HTTP transport, which CanDoAPIRoute's exact (method, path) match cannot gate. The token middleware therefore skips the route check for /api/v1/mcp and any sub-path; the actual authorization is delegated to an inline HasMCPAccess() call in the MCP handler (added in the next task). Fixtures gain two MCP tokens for user 1: one mcp-only and one with mcp:access plus projects read scopes for the per-tool scope filter tests.
Mount /api/v1/mcp (and /api/v1/mcp/*) inside the authenticated route
group. Reject JWT-authed requests with 401 (token-only policy), reject
API tokens without the mcp:access scope with 403, and propagate the
authed *user.User + *models.APIToken to r.Context() via typed keys so
downstream tool handlers can pull them out without depending on Echo.
The MCP protocol — JSON-RPC framing, Mcp-Session-Id management, SSE
streaming — is delegated to github.com/modelcontextprotocol/go-sdk
v1.6.1. tools/list returns {"tools": []} since no tools are registered
yet.
Define the Op bitmask, the Resource struct, the package-level Register function, and the Dispatch entry point that future tasks will use to expose CRUD resources over MCP. No resources are registered yet. Op carries the CRUD-op identity, knows its api-token permission string (matching apiTokenRoutes exactly), and knows its tool-name suffix. Resource.Inputs maps each enabled op to a pointer-to-zero of the wrapper type the dispatcher will allocate and unmarshal into. Register validates the resource shape and populates a tool-name lookup table so the dispatcher never has to string-parse names like task_comments_read_all. Dispatch threads the user from ctx, allocates a fresh wrapper, unmarshals arguments, asks the wrapper to copy itself onto a fresh model via the inputAdapter seam (which Task 4 will populate with real implementations), and forwards to the corresponding handler.Do* function. The Do* calls go through a swappable crudFuncs struct so the unit tests can verify dispatch routing without standing up the database.
Wires the projects resource into the MCP server end-to-end. The five project tools (create, read_one, read_all, update, delete) are now visible in tools/list and dispatch through handler.Do* like the REST layer. - Add ProjectCreateInput / ProjectUpdateInput in inputs.go with jsonschema tags covering only the writable fields the model honours (title, description, identifier, hex_color, parent_project_id, position, is_archived, is_favorite); computed fields like Owner and MaxPermission are intentionally absent so the SDK-reflected schema stays narrow. - Add resources.go with a sync.Once-guarded RegisterResources(), and an installTools helper that registers tools per (resource, op) on the *mcp.Server via a generic addTool[In inputAdapter] helper. The handler maps domain failures (permission denials, missing rows, validation) to IsError tool results per the SDK convention. - Add DispatchTyped in dispatcher.go so the AddTool handler can hand a pre-unmarshalled wrapper to the dispatcher without a JSON round-trip. The existing Dispatch (raw JSON path) delegates to a shared dispatchPrepared. - Wire RegisterResources() + installTools() into newServer() so each new MCP session inherits the static tool set. - Add fixture token 11 (mcp:access + projects:*) for the full-scope integration tests; bump TestAPIToken_ReadAll's expected count. - Refresh TestMCP_ToolsListEmpty into TestMCP_ToolsListReturnsRegisteredResources, asserting the five projects_* tools are present (Task 6 will introduce scope-based filtering of this list). - Add pkg/webtests/mcp_projects_test.go covering tools/list, create/read_one/read_all/update/delete happy paths, schema-validation failure on missing required title, permission denial on a forbidden project, and nonexistent-id lookup.
Filter MCP tool visibility and invocation by the requesting API token's (group, permission) scopes. tools/list now returns only the tools the token's APIPermissions authorise; tools/call additionally re-checks the scope in the dispatcher as defence-in-depth, so a session created with one token cannot be reused to invoke tools that token never had access to. The per-session filter runs at session-init via the StreamableHTTPHandler getServer factory (which the SDK calls once per session, before caching the *mcp.Server). The dispatcher check runs on every tools/call and returns ErrScopeDenied, which the AddTool wrapper renders as an IsError tool result.
Registers tasks, labels, teams, task_comments and task_assignees through
the MCP tool surface, completing the v1 resource list from the plan:
* tasks : create / read_one / update / delete (read_all omitted;
models.Task.ReadAll is a stub — TaskCollection is OOS)
* labels : full CRUD
* teams : full CRUD
* tasks_comments : full CRUD, install-time gated on
config.ServiceEnableTaskComments
* tasks_assignees : create / read_all / delete only (REST exposes no
read_one or update)
Per-resource input wrappers carry the path-param fields (task_id,
user_id) explicitly so MCP callers can provide them as JSON args.
installToolsForToken fans out to one installer per resource; the
generics-bound addTool keeps per-(resource, op) call sites at compile
time. The api_tokens.yml fixture extends token 11 to cover the new
scopes; token count stays at 5 for user 1 so existing token-listing
tests are unaffected.
Integration tests per resource cover tools/list visibility, at least
one successful create or read_all, and a permission denial scenario.
Preview DeploymentPreview deployments for this PR are available at:
The preview environment will start automatically on first visit. Subsequent pushes to this PR will update the Run locally with Dockerdocker pull ghcr.io/go-vikunja/vikunja:pr-2800
docker run -p 3456:3456 ghcr.io/go-vikunja/vikunja:pr-2800Last updated for commit 9456223 |
|
auggie review |
🤖 Augment PR SummarySummary: This PR adds an MCP (Model Context Protocol) server endpoint at Changes:
🤖 Was this summary useful? React with 👍 or 👎 |
| return nil, errors.New("mcp: nil input prototype") | ||
| } | ||
| t := reflect.TypeOf(proto) | ||
| if t.Kind() != reflect.Pointer { |
There was a problem hiding this comment.
In pkg/modules/mcp/dispatcher.go:232, the pointer-kind check uses reflect.Pointer, but the standard reflect.Kind constant is reflect.Ptr, so this is likely to not compile or to mis-detect pointer inputs. Other locations where this applies: pkg/modules/mcp/inputs.go:127, pkg/modules/mcp/inputs.go:181, pkg/modules/mcp/inputs.go:190, pkg/modules/mcp/inputs.go:219, pkg/modules/mcp/inputs.go:238.
Severity: high
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
reflect.Pointer is the canonical name for the pointer kind in reflect since Go 1.18 — reflect.Ptr is an older alias kept for backward compatibility, and both compare identically. The code compiles cleanly and the full mage test:web / mage test:feature suites pass against this branch, so no change needed on this thread.
| } | ||
| srcVal = srcVal.Elem() | ||
| } | ||
| if srcVal.IsZero() { |
There was a problem hiding this comment.
In pkg/modules/mcp/inputs.go:225, skipping all IsZero() values means MCP update wrappers can’t apply explicit false/0 updates (e.g. tasks_update cannot set done=false, and numeric fields can’t be set back to 0). This likely makes MCP update semantics diverge from the REST API for common “unset/clear” operations.
Severity: high
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.
There was a problem hiding this comment.
Good catch — addressed in b0bd8ab. copyByJSONTag now distinguishes value-typed sources (still skipped on IsZero for partial-update semantics) from pointer-typed sources (always propagated when non-nil, even on the zero pointee). TaskUpdateInput and ProjectUpdateInput switched their bool / numeric fields to pointers (*bool, *int64, etc.), so callers can now explicitly send done: false, is_archived: false, priority: 0, etc. Integration tests TestMCP_Tasks_UpdateClearsDone and TestMCP_Projects_UpdateClearsArchived cover the regression end-to-end.
copyByJSONTag previously skipped any IsZero value, which made it impossible for tasks_update / projects_update to flip done from true to false, reset priority/percent_done to 0, or unarchive a project. A non-nil pointer src is now the unambiguous "caller supplied this" signal: dereferenced values are written through even when zero, while value-typed src fields keep the partial-update semantics. The affected wrapper fields (Done, IsArchived, IsFavorite, Priority, PercentDone, RepeatAfter, RepeatMode, BucketID, CoverImageAttachmentID, ParentProjectID, Position) move to pointer types so the JSON Schema still marks them optional.
Adds a Model Context Protocol server at
POST/GET/DELETE /api/v1/mcpso AI clients (Claude Desktop, Cursor, etc.) can drive Vikunja through its existing CRUD API.Built on
github.com/modelcontextprotocol/go-sdkv1.6.1. Tools dispatch through the existinghandler.Do*functions, so permission checks, transactions, and events stay on the existing code path. Three-layer auth:mcp:accesstoken scope gates the endpoint, per-resource token scopes gate each tool, and modelCan*methods do user-level authorization.v1 surface:
projects,tasks,labels,teams,task_comments(config-gated),task_assignees. Token-only (no JWT) — MCP clients paste a URL + API token.closes #2499.